"""
general_xrd.py - A set of python functions useful for doing calculations pertinent to XRD analaysis

Original author: Brendan Arnold, TCD - 2008

Version: 0.1 (some errors may exist!)

Do whatever you like with this code so long as credit for original basis is given in source.

The easiest way to import these functions into your code is to include it in
the same directory as your scripts and use 'from general_xrd import *'. For
more detailed instructions see http://docs.python.org/tut/node8.html
"""

from math import sin, cos, radians, degrees, sqrt, acos

COMMON_XRAY_WAVELENGTHS = {
  'CuKa1': 1.5405981E-10,
  'CuKa2': 1.54443E-10,
  'CuKb1': 1.39225E-10,
  'WLa1': 1.47642E-10,
  'WLa2': 1.48748E-10
}

def parse_miller_indices(p):
  """Pass in an interable of length 3 or 4. Tries to covnert each index to an integer and returns a tuple h, k, l

  Usage: parse_miller_indices([h,k,(j),l])"""
  try:
    n = len(p)
    if 5 < n < 2:
      raise
  except:
    raise ValueError, 'Argument must be iterable of 3 or 4 integers'
  if n == 3:
    h, k, l = p
  elif n == 4:
    # j is not used
    h, k, j, l = p
  try:
    h = int(h)
    k = int(k)
    l = int(l)
  except:
    raise ValueError, 'Values must be convertable to integers'
  return h, k, l

# The following functions come from Appendix 3 of Cullity 2nd ed. 'Elements of
# X-ray Diffraction' from Addison Wesley

def _S(a, b, c, alpha, beta, gamma):
  """Returns the 'S' convenience variables for triclinic calculations in this order:
  S11, S22, S33, S12, S23, S13

  Usage: S11, S22, S33, S12, S23, S13 = _S(a, b, c, alpha, beta, gamma)"""
  sa, sb, sg, ca, cb, cg = sin(radians(alpha)), sin(radians(beta)), sin(radians(gamma)), \
    cos(radians(alpha)), cos(radians(beta)), cos(radians(gamma))
  S11 = b**2*c**2*sa**2
  S22 = a**2*c**2*sb**2
  S33 = a**2*b**2*sg**2
  S12 = a*b*c**2*(ca*cb-cg)
  S23 = a**2*b*c*(cb*cg-ca)
  S13 = a*b**2*c*(cg*ca-cb)
  return S11, S22, S33, S12, S23, S13
  
def calc_volume_cubic(a=None):
  """Pass in a to get volume of unit cell

  Usage: calc_volume_cubic(a)"""
  return a**3

def calc_volume_tetragonal(a=None, c=None):
  """Pass in a, c to get volume of unit cell

  Usage: calc_volume_tetragonal(a, c)"""
  return c*a**2

def calc_volume_hexagonal(a=None, c=None):
  """Pass in a, c to get volume of unit cell

  Usage: calc_volume_hexagonal(a, c)"""
  return c*(a**2)*sqrt(3)/2

def calc_volume_rhombohedral(a=None, alpha=None):
  """Pass in a, alpha to get volume of unit cell

  Usage: calc_volume_rhombohedral(a, alpha)"""
  ca = cos(radians(alpha))
  return (a**3)*sqrt(1 - 3*ca**2 + 2*ca**3)

def calc_volume_orthorhombic(a=None, b=None, c=None):
  """Pass in a, b, c to get volume of unit cell

  Usage: calc_volume_orthorhombic(a, b, c)"""
  return a*b*c

def calc_volume_monoclinic(a=None, b=None, c=None, beta=None):
  """Pass in a, b, c, beta to get volume of unit cell

  Usage: calc_volume_monoclinic(a, b, c, beta)"""
  sb = sin(radians(beta))
  return a*b*c*sb

def calc_volume_triclinic(a=None, b=None, c=None, alpha=None, beta=None, gamma=None):
  """Pass in a, b, c, alpha, beta, gamma to get volume of unit cell

  Usage: calc_volume_triclinic(a, b, c, alpha, beta, gamma)"""
  ca, cb, cg = cos(radians(alpha)), cos(radians(beta)), cos(radians(gamma))
  return a*b*c*sqrt(1 - ca**2 - cb**2 - cg**2 + 2*ca*cb*cg)

def calc_plane_spacing_cubic(p=None, a=None):
  """Pass in a plane [h,k,l] and a to get the plane spacing d

  Usage: calc_plane_spacing_cubic([h,k,l], a)"""
  h, k, l = parse_miller_indices(p)
  reciprocal_d2 = 1.0*(h**2 + k**2 + l**2)/a**2
  return sqrt(1.0/reciprocal_d2)

def calc_plane_spacing_tetragonal(p=None, a=None, c=None):
  """Pass in a plane [h,k,l], a and c to get the plane spacing d

  Usage: calc_plane_spacing_tetragonal([h,k,l], a, c)"""
  h, k, l = parse_miller_indices(p)
  reciprocal_d2 = 1.0*(h**2+k**2)/a**2 + 1.0*l**2/c**2
  return sqrt(1.0/reciprocal_d2)

def calc_plane_spacing_hexagonal(p=None, a=None, c=None):
  """Pass in a plane [h,k,l], a and c to get the plane spacing d

  Usage: calc_plane_spacing_hexagonal([h,k,l], a, c)"""
  h, k, l = parse_miller_indices(p)
  reciprocal_d2 = (4.0/3.0) * 1.0*(h**2+h*k+k**2)/a**2 + 1.0*l**2/c**2
  return sqrt(1.0/reciprocal_d2)
  
def calc_plane_spacing_rhombohedral(p=None, a=None, alpha=None):
  """Pass in a plane [h,k,l], a and alpha to get the plane spacing d

  Usage: calc_plane_spacing_rhombohedral([h,k,l], a, alpha)"""
  h, k, l = parse_miller_indices(p)
  sa, ca = sin(radians(alpha)), cos(radians(alpha))
  reciprocal_d2 = 1.0*( (h**2+k**2+l**2)*sa**2 + 2*(h*k+k*l+h*l)*(ca**2-ca) ) / \
    a**2*(1-3*ca**2+2*ca**3)
  return sqrt(1.0/reciprocal_d2)

def calc_plane_spacing_orthorhombic(p=None, a=None, b=None, c=None):
  """Pass in a plane [h,k,l], a, b and c to get the plane spacing d

  Usage: calc_plane_spacing_orthorhombic([h,k,l], a, b, c)"""
  h, k, l = parse_miller_indices(p)
  reciprocal_d2 = 1.0*h**2/a**2 + 1.0*k**2/b**2 + 1.0*l**2/c**2
  return sqrt(1.0/reciprocal_d2)

def calc_plane_spacing_monoclinic(p=None, a=None, b=None, c=None, beta=None):
  """Pass in a plane [h,k,l], a, b, c and beta to get the plane spacing d

  Usage: calc_plane_spacing_monoclinic([h,k,l], a, b, c, beta)"""
  h, k, l = parse_miller_indices(p)
  sb, cb = sin(radians(beta)), cos(radians(beta))
  reciprocal_d2 = (1.0/sb**2) * (1.0*h**2/a**2 + 1.0*k**2*sb**2/b**2 + 1.0*l**2/c**2 - 2.0*h*l*cb/(a*c))
  return sqrt(1.0/reciprocal_d2)

def calc_plane_spacing_triclinic(p, a, b, c, alpha, beta, gamma):
  """Pass in a plane [h,k,l], a, b, c, alpha, beta, gamma to get the plane spacing d

  Usage: calc_plane_spacing_triclinic([h,k,l], a, b, c, alpha, beta, gamma)"""
  h, k, l = parse_miller_indices(p)
  S11, S22, S33, S12, S23, S13 = _S(a, b, c, alpha, beta, gamma)
  V = calc_volume_triclinic(a, b, c, alpha, beta, gamma)
  reciprocal_d2 = 1.0/V**2 * (S11*h**2 + S22*k**2 + S33*l**2 + 2*S12*h*k + 2*S23*k*l + 2*S13*h*l) 
  return sqrt(1.0/reciprocal_d2)


def calc_plane_angle_cubic(p1=None, p2=None):
  """Supply two sets of miller indices, calculates the angle in degrees between each

  Usage: calc_plane_angle_cubic([h,k,l], [h,k,l])"""
  h1, k1, l1 = parse_miller_indices(p1)
  h2, k2, l2 = parse_miller_indices(p2)
  cos_phi = 1.0 * (h1*h2+k1*k2+l1*l2) / sqrt((h1**2+k1**2+l1**2)*(h2**2+k2**2+l2**2))
  # return cos_phi
  return degrees(acos(cos_phi))

def calc_plane_angle_tetragonal(p1=None, p2=None, a=None, c=None):
  """Supply two sets of miller indices, calculates the angle in degrees between each, as well as a and c magnitudes

  Usage: calc_plane_angle_tetragonal([h,k,l], [h,k,l], a, c)"""
  h1, k1, l1 = parse_miller_indices(p1)
  h2, k2, l2 = parse_miller_indices(p2)
  cos_phi = ( 1.0*(h1*h2+k1*k2)/a**2 + 1.0*l1*l2/c**2 ) / \
    sqrt( (1.0*(h1**2+k1**2)/a**2 + 1.0*l1**2/c**2)*(1.0*(h2**2+k2**2)/a**2 + 1.0*l2**2/c**2) )
  return degrees(acos(cos_phi))

def calc_plane_angle_hexagonal(p1=None, p2=None, a=None, c=None):
  """Supply two sets of miller indices, calculates the angle in degrees between each, as well as a and c magnitudes

  Usage: calc_plane_angle_hexagonal([h,k,l], [h,k,l], a, c)"""
  h1, k1, l1 = parse_miller_indices(p1)
  h2, k2, l2 = parse_miller_indices(p2)
  cos_phi = (h1*h2+k1*k2+0.5*h1*k2+0.5*h2*k1+(l1*l2*3.0*a**2)/(4.0*c**2)) / \
    sqrt((h1**2+k1**2+h1*k1+(3.0/4.0)*(1.0*a*l1/c)**2)*(h2**2+k2**2+h2*k2+(3.0/4.0)*(1.0*a*l2/c)**2))
  return degrees(acos(cos_phi))

def calc_plane_angle_rhombohedral(p1=None, p2=None, a=None, alpha=None):
  """Supply two sets of miller indices, a and alpha to calculate the angle in degrees between each

  Usage: calc_plane_angle_rhombohedral([h,k,l], [h,k,l], d1, d2, a, alpha)"""
  h1, k1, l1 = parse_miller_indices(p1)
  h2, k2, l2 = parse_miller_indices(p2)
  d1 = calc_plane_spacing_rhombohedral(p1, a, alpha)
  d2 = calc_plane_spacing_rhombohedral(p2, a, alpha)
  V = calc_volume_rhombohedral(a, alpha)
  sa, ca = sin(radians(alpha)), cos(radians(alpha))
  cos_phi = (1.0*(d1*d2*a**4)/V**2) * \
    ((h1*h2+k1*k2+l1*l2)*sa**2 + (ca**2 - ca)*(k1*l2+k2*l1+l1*h2+l2*h1+h1*k2+h2*k1))
  return degrees(acos(cos_phi))

def calc_plane_angle_orthorhombic(p1=None, p2=None, a=None, b=None, c=None):
  """Supply two sets of miller indices, a, b and c to calculate the angle in degrees between each

  Usage: calc_plane_angle_orthorhombic([h,k,l], [h,k,l], a, b, c)"""
  h1, k1, l1 = parse_miller_indices(p1)
  h2, k2, l2 = parse_miller_indices(p2)
  cos_phi = ( 1.0*h1*h2/a**2 + 1.0*k1*k2/b**2 + 1.0*l1*l2/c**2 ) / \
    sqrt((1.0*h1**2/a**2 + 1.0*k1**2/b**2 + 1.0*l1**2/c**2)*(1.0*h2**2/a**2 + 1.0*k2**2/b**2 + 1.0*l2**2/c**2))
  return degrees(acos(cos_phi))

def calc_plane_angle_monoclinic(p1=None, p2=None, a=None, b=None, c=None, beta=None):
  """Supply two sets of miller indices, a, b, c and beta to calculate the angle in degrees between each

  Usage: calc_plane_angle_monoclinic([h,k,l], [h,k,l], d1, d2, a, b, c, beta)"""
  h1, k1, l1 = parse_miller_indices(p1)
  h2, k2, l2 = parse_miller_indices(p2)
  d1, d2 = calc_plane_spacing_monoclinic(p1, a, b, c, beta), \
    calc_plane_spacing_monoclinic(p2, a, b, c, beta)
  sb, cb = sin(radians(beta)), cos(radians(beta))
  cos_phi = (1.0*d1*d2/sb**2) * (1.0*h1*h2/a**2 + 1.0*k1*k2*sb**2/b**2 + 1.0*l1*l2/c**2 - 1.0*(l1*h2+l2*h1)*cb/(a*c))
  return degrees(acos(cos_phi))

def calc_plane_angle_triclinic(p1=None, p2=None, a=None, b=None, c=None, alpha=None, beta=None, gamma=None):
  """Supply two sets of miller indices, a, b, c, alpha, beta and gamma to calculate the angle in degrees between each

  Usage: calc_plane_angle_triclinic([h,k,l], [h,k,l], a, b, c, alpha, beta, gamma)"""
  h1, k1, l1 = parse_miller_indices(p1)
  h2, k2, l2 = parse_miller_indices(p2)
  S11, S22, S33, S12, S23, S13 = _S(a, b, c, alpha, beta, gamma)
  d1, d2 = calc_plane_spacing_triclinic(p1, a, b, c, alpha, beta, gamma), \
    calc_plane_spacing_triclinic(p2, a, b, c, alpha, beta, gamma)
  V = calc_volume_triclinic(a, b, c, alpha, beta, gamma)
  cos_phi = (1.0*d1*d2/V**2) * (S11*h1*h2 + S22*k1*k2 + S33*l1*l2 + S23*(k1*l2+k2*l1) + S13*(l1*h2+l2*h1) + S12*(h1*k2+h2*k1))
  return degrees(acos(cos_phi))

# Class based functionality

class _Lattice(object):
  """Parent class for lattice objects"""
  def set_lattice_param(self, val, param):
    param = str(param)
    if hasattr(self, param):
      try:
        val = float(val)
      except:
        raise ValueError, 'Lattice parameters must be convertable to a float'
      setattr(self, val, param)
    else:
      raise ValueError, 'Lattice of type %s does not require lattice parameter %s' % (self.type, param)
  def get_lattice_param(self, param):
    param = str(param)
    if hasattr(self, param):
      return getattr(self, param)
    else:
      raise ValueError, 'Lattice of type %s does not have lattice parameter %s' % (self.type, param)

class CubicLattice(_Lattice):
  """Create cubic lattice object, pass a on creating an instance"""
  def __init__(self, a=None):
    self.a = float(a)
  def get_description(self):
    return '%s (a=%.3f)' % (self.type, self.a)
  description = property(get_description)
  def get_type(self):
    return 'cubic'
  type = property(get_type)
  def get_cell_volume(self):
    return calc_volume_cubic(self.a)
  cell_volume = property(get_cell_volume)
  def get_angle_between_planes(self, p1, p2):
    return calc_plane_angle_cubic(p1, p2)

class TetragonalLattice(_Lattice):
  """Create tetragonal lattice object, pass a and c on creating an instance"""
  def __init__(self, a=None, c=None):
    self.a = float(a)
    self.c = float(c)
  def get_description(self):
    return '%s (a=%.3f, c=%.3f)' % (self.type, self.a, self.c)
  description = property(get_description)
  def get_type(self):
    return 'tetragonal'
  type = property(get_type)
  def get_cell_volume(self):
    return calc_volume_tetragonal(self.a, self.c)
  cell_volume = property(get_cell_volume)
  def get_angle_between_planes(self, p1, p2):
    return calc_plane_angle_tetragonal(p1, p2, self.a, self.c)

class HexagonalLattice(_Lattice):
  """Create hexagonal lattice object, pass a and c on creating an instance"""
  def __init__(self, a=None, c=None):
    self.a = float(a)
    self.c = float(c)
  def get_description(self):
    return '%s (a=%.3f, c=%.3f)' % (self.type, self.a, self.c)
  description = property(get_description)
  def get_type(self):
    return 'hexagonal'
  type = property(get_type)
  def get_cell_volume(self):
    return calc_volume_hexagonal(self.a, self.c)
  cell_volume = property(get_cell_volume)
  def get_angle_between_planes(self, p1, p2):
    return calc_plane_angle_hexagonal(p1, p2, self.a, self.c)

class RhombohedralLattice(_Lattice):
  """Create rhombohedral lattice object, pass a and alpha on creating an instance"""
  def __init__(self, a=None, alpha=None):
    self.a = float(a)
    self.alpha = float(alpha)
  def get_description(self):
    return '%s (a=%.3f, alpha=%.3f)' % (self.type, self.a, self.alpha)
  description = property(get_description)
  def get_type(self):
    return 'rhombohedral'
  type = property(get_type)
  def get_cell_volume(self):
    return calc_volume_rhombohedral(self.a, self.alpha)
  cell_volume = property(get_cell_volume)
  def get_angle_between_planes(self, p1, p2):
    return calc_plane_angle_rhombohedral(p1, p2, self.a, self.alpha)

class OrthorhombicLattice(_Lattice):
  """Create orthorhombic lattice object, pass a, b and c on creating an instance"""
  def __init__(self, a=None, b=None, c=None):
    self.a = float(a)
    self.b = float(b)
    self.c = float(c)
  def get_description(self):
    return '%s (a=%.3f, b=%.3f, c=%.3f)' % (self.type, self.a, self.b, self.c)
  description = property(get_description)
  def get_type(self):
    return 'orthorhombic'
  type = property(get_type)
  def get_cell_volume(self):
    return calc_volume_orthorhombic(self.a, self.b, self.c)
  cell_volume = property(get_cell_volume)
  def get_angle_between_planes(self, p1, p2):
    return calc_plane_angle_orthorhombic(p1, p2, self.a, self.b, self.c)

class MonoclinicLattice(_Lattice):
  """Create monoclinic lattice object, pass a, b, c and beta on creating an instance"""
  def __init__(self, a=None, b=None, c=None, beta=None):
    self.a = float(a)
    self.b = float(b)
    self.c = float(c)
    self.beta = float(beta)
  def get_description(self):
    return '%s (a=%.3f, b=%.3f, c=%.3f, beta=%.3f)' % (self.type, self.a, self.b, self.c, self.beta)
  description = property(get_description)
  def get_type(self):
    return 'monoclinic'
  type = property(get_type)
  def get_cell_volume(self):
    return calc_volume_monoclinic(self.a, self.b, self.c, self.beta)
  cell_volume = property(get_cell_volume)
  def get_angle_between_planes(self, p1, p2):
    return calc_plane_angle_monoclinic(p1, p2, self.a, self.b, self.c, self.beta)

class TriclinicLattice(_Lattice):
  """Create triclinic lattice object, pass a, b, c, alpha, beta and gamma on creating an instance"""
  def __init__(self, a=None, b=None, c=None, alpha=None, beta=None, gamma=None):
    self.a = float(a)
    self.b = float(b)
    self.c = float(c)
    self.beta = float(beta)
    self.alpha = float(alpha)
    self.gamma = float(gamma)
  def get_description(self):
    return '%s (a=%.3f, b=%.3f, c=%.3f, alpha=%.3f, beta=%.3f, gamma=%.3f)' % (self.type, self.a, self.b, self.c, self.alpha, self.beta, self.gamma)
  description = property(get_description)
  def get_type(self):
    return 'triclinic'
  type = property(get_type)
  def get_cell_volume(self):
    return calc_volume_triclinic(self.a, self.b, self.c, self.alpha, self.beta, self.gamma)
  cell_volume = property(get_cell_volume)
  def get_angle_between_planes(self, p1, p2):
    return calc_plane_angle_triclinic(p1, p2, self.a, self.b, self.c, self.alpha, self.beta, self.gamma)
